package com.dimples.dd.system.framework.sms.core.client.impl;

import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.date.format.FastDateFormat;
import cn.hutool.core.exceptions.ExceptionUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.crypto.digest.DigestUtil;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.dimples.dd.common.core.KeyValue;
import com.dimples.dd.common.exception.BizException;
import com.dimples.dd.common.util.collection.CollectionUtils;
import com.dimples.dd.common.util.collection.MapUtils;
import com.dimples.dd.common.util.json.JsonUtils;
import com.dimples.dd.system.framework.sms.core.client.dto.SmsReceiveRespDTO;
import com.dimples.dd.system.framework.sms.core.client.dto.SmsSendRespDTO;
import com.dimples.dd.system.framework.sms.core.client.dto.SmsTemplateRespDTO;
import com.dimples.dd.system.framework.sms.core.enums.SmsTemplateAuditStatusEnum;
import com.dimples.dd.system.framework.sms.core.property.SmsChannelProperties;
import com.google.common.annotations.VisibleForTesting;
import lombok.Getter;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.springframework.http.HttpMethod;

import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.*;


/**
 * 阿里短信客户端的实现类
 *
 * @author zhongyj <1126834403@qq.com><br/>
 * @date 2024/10/28
 */
@Slf4j
public class AliyunSmsClient extends AbstractSmsClient {

    private static final String URL = "https://dysmsapi.aliyuncs.com";
    private static final String HOST = "dysmsapi.aliyuncs.com";
    private static final String VERSION = "2017-05-25";

    private static final String RESPONSE_CODE_SUCCESS = "OK";

    /**
     * 签名协议
     */
    private static final String ALGORITHM = "ACS3-HMAC-SHA256";

    public AliyunSmsClient(SmsChannelProperties properties) {
        super(properties);
        Assert.notEmpty(properties.getApiKey(), "apiKey 不能为空");
        Assert.notEmpty(properties.getApiSecret(), "apiSecret 不能为空");
    }

    @Override
    protected void doInit() {
    }

    @Override
    public SmsSendRespDTO sendSms(Long sendLogId, String mobile, String apiTemplateId, List<KeyValue<String, Object>> templateParams) {
        Assert.notBlank(properties.getSignature(), "短信签名不能为空");
        // 1. 执行请求
        // 参考链接 https://api.aliyun.com/document/Dysmsapi/2017-05-25/SendSms
        TreeMap<String, Object> queryParam = new TreeMap<>();
        queryParam.put("PhoneNumbers", mobile);
        queryParam.put("SignName", properties.getSignature());
        queryParam.put("TemplateCode", apiTemplateId);
        queryParam.put("TemplateParam", JsonUtils.toJsonString(MapUtils.convertMap(templateParams)));
        queryParam.put("OutId", sendLogId);
        JSONObject response = request("SendSms", queryParam);

        // 2. 解析请求
        return new SmsSendRespDTO()
                .setSuccess(Objects.equals(response.getStr("Code"), RESPONSE_CODE_SUCCESS))
                .setSerialNo(response.getStr("BizId"))
                .setApiRequestId(response.getStr("RequestId"))
                .setApiCode(response.getStr("Code"))
                .setApiMsg(response.getStr("Message"));
    }

    @Override
    public List<SmsReceiveRespDTO> parseSmsReceiveStatus(String text) {
        JSONArray statuses = JSONUtil.parseArray(text);
        // 字段参考
        return CollectionUtils.convertList(statuses, status -> {
            JSONObject statusObj = (JSONObject) status;
            return new SmsReceiveRespDTO()
                    .setSuccess(statusObj.getBool("success")) // 是否接收成功
                    .setErrorCode(statusObj.getStr("err_code")) // 状态报告编码
                    .setErrorMsg(statusObj.getStr("err_msg")) // 状态报告说明
                    .setMobile(statusObj.getStr("phone_number")) // 手机号
                    .setReceiveTime(statusObj.getLocalDateTime("report_time", null)) // 状态报告时间
                    .setSerialNo(statusObj.getStr("biz_id")) // 发送序列号
                    .setLogId(statusObj.getLong("out_id")); // 用户序列号
        });
    }

    @Override
    public SmsTemplateRespDTO getSmsTemplate(String apiTemplateId) throws Throwable {
        // 1. 执行请求
        // 参考链接 https://api.aliyun.com/document/Dysmsapi/2017-05-25/QuerySmsTemplate
        TreeMap<String, Object> queryParam = new TreeMap<>();
        queryParam.put("TemplateCode", apiTemplateId);
        JSONObject response = request("QuerySmsTemplate", queryParam);

        // 2.1 请求失败
        String code = response.getStr("Code");
        if (ObjectUtil.notEqual(code, RESPONSE_CODE_SUCCESS)) {
            log.error("[getSmsTemplate][模版编号({}) 响应不正确({})]", apiTemplateId, response);
            return null;
        }
        // 2.2 请求成功
        return new SmsTemplateRespDTO()
                .setId(response.getStr("TemplateCode"))
                .setContent(response.getStr("TemplateContent"))
                .setAuditStatus(convertSmsTemplateAuditStatus(response.getInt("TemplateStatus")))
                .setAuditReason(response.getStr("Reason"));
    }

    @VisibleForTesting
    Integer convertSmsTemplateAuditStatus(Integer templateStatus) {
        return switch (templateStatus) {
            case 0 -> SmsTemplateAuditStatusEnum.CHECKING.getStatus();
            case 1 -> SmsTemplateAuditStatusEnum.SUCCESS.getStatus();
            case 2 -> SmsTemplateAuditStatusEnum.FAIL.getStatus();
            default -> throw new IllegalArgumentException(String.format("未知审核状态(%d)", templateStatus));
        };
    }

    /**
     * 请求阿里云短信
     *
     * @param apiName     请求的 API 名称
     * @param queryParams 请求参数
     * @return 请求结果
     * @see <a href="https://help.aliyun.com/zh/sdk/product-overview/v3-request-structure-and-signature">V3 版本请求体&签名机制</>
     */
    private JSONObject request(String apiName, TreeMap<String, Object> queryParams) {
        Request request = new Request(HttpMethod.POST.name(), StrUtil.SLASH, HOST, apiName, VERSION);

        // 调用API所需要的参数，参数按照参数名的字符代码升序排列，具有重复名称的参数应按值进行排序。
        request.queryParam.putAll(queryParams);
        // 签名过程
        getAuthorization(request, properties.getApiKey(), properties.getApiSecret());
        // 调用API
        String responseBody = callPostApi(request);
        return JSONUtil.parseObj(responseBody);
    }

    /**
     * 对指定的字符串进行 URL 编码，并对特定的字符进行替换，以符合URL编码规范
     *
     * @param str 需要进行 URL 编码的字符串
     * @return 编码后的字符串
     */
    @SneakyThrows
    private static String percentCode(String str) {
        Assert.notNull(str, "str 不能为空");
        return URLEncoder.encode(str, StandardCharsets.UTF_8)
                .replace("+", "%20") // 加号 "+" 被替换为 "%20"
                .replace("*", "%2A") // 星号 "*" 被替换为 "%2A"
                .replace("%7E", "~"); // 波浪号 "%7E" 被替换为 "~"
    }

    @Getter
    private static class Request {
        // HTTP Method
        private final String httpMethod;
        // 请求路径，当资源路径为空时，使用正斜杠(/)作为CanonicalURI
        private final String canonicalUri;
        // endpoint
        private final String host;
        // API name
        private final String xAcsAction;
        // API version
        private final String xAcsVersion;
        // headers
        TreeMap<String, Object> headers = new TreeMap<>();
        // 调用API所需要的参数，参数位置在body。Json字符串
        String body;
        // 调用API所需要的参数，参数位置在query，参数按照参数名的字符代码升序排列
        TreeMap<String, Object> queryParam = new TreeMap<>();

        public Request(String httpMethod, String canonicalUri, String host, String xAcsAction, String xAcsVersion) {
            this.httpMethod = httpMethod;
            this.canonicalUri = canonicalUri;
            this.host = host;
            this.xAcsAction = xAcsAction;
            this.xAcsVersion = xAcsVersion;
            initBuilder();
        }

        // init headers
        private void initBuilder() {
            headers.put("host", host);
            headers.put("x-acs-action", xAcsAction);
            headers.put("x-acs-version", xAcsVersion);
            headers.put("x-acs-date", DateUtil.format(new Date(), FastDateFormat.getInstance(DatePattern.UTC_PATTERN, TimeZone.getTimeZone("GMT"))));
            headers.put("x-acs-signature-nonce", UUID.randomUUID().toString());
        }
    }

    private static String callPostApi(Request request) {
        try {
            // 通过HttpClient发送请求
            String url = URL + request.canonicalUri;
            URIBuilder uriBuilder = new URIBuilder(url);
            // 添加请求参数
            for (Map.Entry<String, Object> entry : request.queryParam.entrySet()) {
                uriBuilder.addParameter(entry.getKey(), String.valueOf(entry.getValue()));
            }
            HttpUriRequest httpRequest;
            HttpPost httpPost = new HttpPost(uriBuilder.build());
            if (request.body != null) {
                httpPost.setEntity(new StringEntity(request.body, ContentType.APPLICATION_JSON));
            }
            httpRequest = httpPost;

            // 添加http请求头
            for (Map.Entry<String, Object> entry : request.headers.entrySet()) {
                httpRequest.addHeader(entry.getKey(), String.valueOf(entry.getValue()));
            }
            // 发送请求
            try (CloseableHttpClient httpClient = HttpClients.createDefault(); CloseableHttpResponse response = httpClient.execute(httpRequest)) {
                return EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
            } catch (IOException e) {
                log.error("Failed to send request: {}", ExceptionUtil.stacktraceToString(e));
            }
        } catch (URISyntaxException e) {
            log.error("Invalid URI syntax: {}", ExceptionUtil.stacktraceToString(e));
        }
        return "";
    }

    /**
     * 该方法用于根据传入的HTTP请求方法、规范化的URI、查询参数等，计算并生成授权信息。
     */
    private static void getAuthorization(Request request, String apiKey, String secretKey) {
        try {
            // 步骤 1：拼接规范请求串
            // 请求参数，当请求的查询字符串为空时，使用空字符串作为规范化查询字符串
            StringBuilder canonicalQueryString = new StringBuilder();
            request.queryParam.entrySet().stream().map(entry -> percentCode(entry.getKey()) + "=" + percentCode(String.valueOf(entry.getValue()))).forEachOrdered(queryPart -> {
                // 如果canonicalQueryString已经不是空的，则在查询参数前添加"&"
                if (StrUtil.isNotBlank(canonicalQueryString)) {
                    canonicalQueryString.append("&");
                }
                canonicalQueryString.append(queryPart);
            });

            // 请求体，当请求正文为空时，比如GET请求，RequestPayload固定为空字符串
            String requestPayload = "";
            if (request.body != null) {
                requestPayload = request.body;
            }

            // 计算请求体的哈希值
            String hashedRequestPayload = DigestUtil.sha256Hex(requestPayload).toLowerCase();
            request.headers.put("x-acs-content-sha256", hashedRequestPayload);
            // 构造请求头，多个规范化消息头，按照消息头名称（小写）的字符代码顺序以升序排列后拼接在一起
            StringBuilder canonicalHeaders = new StringBuilder();
            // 已签名消息头列表，多个请求头名称（小写）按首字母升序排列并以英文分号（;）分隔
            StringBuilder signedHeadersSb = new StringBuilder();
            request.headers.entrySet().stream().filter(entry -> entry.getKey().toLowerCase().startsWith("x-acs-") || entry.getKey().equalsIgnoreCase("host") || entry.getKey().equalsIgnoreCase("content-type")).sorted(Map.Entry.comparingByKey()).forEach(entry -> {
                String lowerKey = entry.getKey().toLowerCase();
                String value = String.valueOf(entry.getValue()).trim();
                canonicalHeaders.append(lowerKey).append(":").append(value).append("\n");
                signedHeadersSb.append(lowerKey).append(";");
            });
            String signedHeaders = signedHeadersSb.substring(0, signedHeadersSb.length() - 1);
            String canonicalRequest = request.httpMethod + "\n" + request.canonicalUri + "\n" + canonicalQueryString + "\n" + canonicalHeaders + "\n" + signedHeaders + "\n" + hashedRequestPayload;

            // 步骤 2：拼接待签名字符串
            String hashedCanonicalRequest = DigestUtil.sha256Hex(canonicalRequest).toLowerCase(); // 计算规范化请求的哈希值
            String stringToSign = ALGORITHM + "\n" + hashedCanonicalRequest;

            // 步骤 3：计算签名
            String signature = SecureUtil.hmacSha256(secretKey).digestHex(stringToSign).toLowerCase();

            // 步骤 4：拼接 Authorization
            String authorization = ALGORITHM + " " + "Credential=" + apiKey + ",SignedHeaders=" + signedHeaders + ",Signature=" + signature;
            request.headers.put("Authorization", authorization);
        } catch (Exception e) {
            // 异常处理
            log.error("Failed to get authorization");
            throw new BizException("Failed to get authorization");
        }
    }
}